from __future__ import absolute_import
import abc
import collections
import decimal

#===============================================================================
# Decorator that register classes to __all__
#===============================================================================
__all__ = []
def export(obj):
    try:
        __all__.append(obj.__name__)
        return obj
    except AttributeError:
        return [export(x) for x in obj]

class HasView(object):
    @classmethod
    def view(cls, obj, force=False, destructive=False):
        '''Return an object that has a predictable interface defined for the 
        abstract class. It may return the obj itself if it already satisfies
        all the requirements or a view to the object's content.'''

        if isinstance(obj, cls):
            return obj
        else:
            if force:
                return cls._forceview_(obj)
            else:
                raise TypeError('object of type %s is not a %s' % (type(obj), cls))

    @classmethod
    def _forceview_(cls, obj):
        '''It is called by view() when force=True'''
        raise TypeError('object of type %s is not a %s' % (type(obj), cls))

#===============================================================================
# Numeric data types (most of them come from 'numbers' module)
#===============================================================================
from numbers import Number as _Number, Real, Integral as _Integral, Complex, Rational

export((Rational, Real, Complex))

@export
class Integer(HasView):
    '''Represent integers'''
    _TYPE = _Integral
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, cls._TYPE)

    @classmethod
    def _forceview_(cls, obj):
        return int(obj)

@export
class Numeric(HasView):
    '''Basic class for all numeric data types. It differs from `Number` as 
    `Number` only support Float and Integer, which are the native JSON types.
    `Numeric` is the abstract class for all numeric types, including complex
    and rational numbers (and possibly others). 
    '''
    _TYPE = _Number
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, cls._TYPE)

@export
class Float(HasView):
    '''Non-integer real numbers. Rational are allowed if they provide a 
    __float__ method.'''

    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        if issubclass(tt, (float, decimal.Decimal)):
            return True
        elif issubclass(tt, (Integer, complex)):
            return False
        elif issubclass(tt, Rational) and hasattr(tt, '__float__'):
            return True
        else:
            return NotImplemented

    @classmethod
    def _forceview_(cls, obj):
        return float(obj)

@export
class Number(HasView):
    '''Numbers compatible with JSON standard: Floats and Integers'''

    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, Integer) or issubclass(tt, Float)

# Simple tests
if __name__ == '__main__':
    import fractions
    assert isinstance(fractions.Fraction(2, 3), Float)
    assert isinstance(1.0, Float)
    assert isinstance(1, Integer)
    assert isinstance(1.0, Numeric)

#===============================================================================
# Values
#===============================================================================
@export
class String(HasView):
    '''A string is essentially a sequence of characters. Since python cannot 
    inspect the type of the sequence elements from the type alone, it can be 
    hard to tell if a given sequence type should behave as a string or not.
    
    We consider all subclasses of basestring and bytearray as strings. If that
    is not the case, the abstract class inspect if all string methods (as of 
    python 2.0) are present. 
    
    
    Notes
    -----

    The abstract class Bytes is related to string as it represents all 
    objects that produce byte data that can be recovered by applying the str()
    built-in to the object. This includes Strings and buffers.'''
    __metaclass__ = abc.ABCMeta
    STRING_METHODS = set(['capitalize', 'center', 'count', 'endswith',
                          'expandtabs', 'find', 'index', 'isalnum', 'isalpha',
                          'isdigit', 'islower', 'isspace', 'istitle', 'isupper',
                          'join', 'ljust', 'lower', 'lstrip', 'replace',
                          'rfind', 'rindex', 'rjust', 'rstrip', 'split',
                          'splitlines', 'startswith', 'strip', 'swapcase',
                          'title', 'translate', 'upper'])

    @classmethod
    def __subclasshook__(cls, tt):
        return (issubclass(cls, (basestring, bytearray)) or
                    cls.STRING_METHODS.issubset(set(dir(tt))))

@export
class Bytes(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, (String, buffer))

@export
class File(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, (file))

@export
class Data(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, (Bytes, File))

if __name__ == '__main__':
    assert isinstance('srds', String)
    assert isinstance('srds', Data)
    assert isinstance(buffer('srds'), Data)
    assert isinstance(u'sdfsdf', String)
    assert not isinstance(tuple('sdfsdfs'), String)

#===============================================================================
# True/False/None
#===============================================================================
@export
class Bool(HasView):
    __metaclass__ = abc.ABCMeta
Bool.register(bool)

@export
class Null(HasView):
    __metaclass__ = abc.ABCMeta
Null.register(type(None))

#===============================================================================
# Generic atomic types
#===============================================================================
@export
class Atom(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, (Number, String, Bool, Null))

@export
class Value(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return (not issubclass(tt, Container))

#===============================================================================
# Container
#===============================================================================
@export
class Object(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, collections.Mapping)

@export
class Array(HasView):
    '''
    Non-string sequences
    '''
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        if issubclass(tt, Data):
            return False
        elif issubclass(tt, collections.Sequence):
            return True
        else:
            return NotImplemented

@export
class Container(HasView):
    __metaclass__ = abc.ABCMeta

    @classmethod
    def __subclasshook__(cls, tt):
        return issubclass(tt, (Array, Object))

if __name__ == '__main__':
    assert isinstance({}, Object)
    assert isinstance([], Array)
    assert isinstance({}, Container)
    assert isinstance([], Container)
    assert not isinstance('', Container)

#===============================================================================
# JSON
#===============================================================================
@export
class JSON(HasView):
    __metaclass__ = abc.ABCMeta

#===============================================================================
# Query functions
#===============================================================================
# Define query functions inside a function to bind cls and name to 
# function's locals()
def factory(name, t_name, f_name, cls):
    def func_t(tt):
        return issubclass(tt, cls)
    func_t.func_name = t_name
    func_t.func_doc = 'Return True if ``tt`` is a valid %s type.' % name

    # Object query function
    def func(obj):
        return isinstance(obj, cls)
    func.func_name = f_name
    func.func_doc = 'Return True if ``obj`` is a valid %s.' % name

    return func_t, func

GLOBALS = globals()
for name in __all__[:]:
    cls, name = GLOBALS[name], name.lower()
    if not isinstance(cls, type):
        continue

    t_name = 'is_%s_t' % name
    f_name = 'is_%s' % name

    GLOBALS[t_name], GLOBALS[f_name] = factory(name, t_name, f_name, cls)
    __all__.extend((t_name, f_name))

if __name__ == '__main__':
    assert is_container({}) #@UndefinedVariable
    assert is_container_t(dict) #@UndefinedVariable
    assert not is_container(1) #@UndefinedVariable

if __name__ == '__main__':
    #print '__all__: \n%s' % ('    \n'.join(__all__))
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)
